Go 语言学习-“面向对象”
struct 基本使用
可以使用 type
关键字定义一个别名
type myint int
func main() {
var val myint = 100
fmt.Printf("%T %d", val, val) // main.myint 100
}
定义一个结构体
type Book struct {
title string
auth string
}
func main() {
var book Book
book.title = "平凡的世界"
book.auth = "路遥"
fmt.Printf("%v", book) // %v 可以打印任意一种类型的格式化
// {平凡的世界 路遥}
}
但是注意,直接拿 Book 当函数形参是值传递的
// 这样修改 book 是不会影响原始值的
func changeBook(book Book) {
book.title = "liyuu_"
}
所以要传递指针进去
func changeBook(book *Book) {
book.title = "liyuu_"
}
func main() {
var book Book
book.title = "平凡的世界"
book.auth = "路遥"
changeBook(&book)
fmt.Printf("%v", book) // {liyuu_ 路遥}
}
Go 中的 “方法”
Go 中的类就是结构体绑定方法,但是不是在结构体内部绑定,而是外部绑定
方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是值接收者,也可以是指针接收者。
Go 没有面向对象,而我们知道常见的 Java、C++ 等语言中,实现类的方法做法都是编译器隐式的给函数加一个 this 指针,而在 Go 里,这个 this 指针需要明确的申明出来,其实和其它 OO 语言并没有很大的区别。
Java 中的:
public class Circle {
private float radius;
private float getArea() {
return 3.14 * radius * radius;
}
}
而到了 Go
type Circle struct {
radius float64
}
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
下面举个例子:
type Book struct {
title string
auth string
}
func (this Book) Show() {
fmt.Printf("%v ", this)
}
func (this Book) SetTitle(nv string) {
this.title = nv
}
func (this Book) GetAuth() string {
return this.auth
}
func main() {
//var book Book;
//book.auth = "吴承恩"
//book.title = "西游记"
book := Book{auth: "吴承恩", title: "西游记"} // 简写
book.Show()
book.SetTitle("人生")
book.Show()
}
但是注意:打印结果是
{西游记 吴承恩} {西游记 吴承恩}
因为上面那个 this 只是一个原结构体的拷贝,所以需要改成指针才能更改原始值(这里不用加 &
就能调用是因为,Go 默认加上 &
了)
func (this *Book) SetTitle(nv string) {
this.title = nv
}
// ...
book.SetTitle("人生")
// 打印结果
// {西游记 吴承恩} {人生 吴承恩}
值接受者和指针接收者
对于值接收者,如果调用者也是值对象,那么会将调用者的值拷贝一份,并执行方法,方法的调用不会影响到调用者值。 如果调用者是指针对象,那么会解引用指针对象为值,然后将解引的对象拷贝一份,然后执行方法。
对于指针接收者,如果调用者是值对象,会使用值的引用来调用方法,上例中,book.SetTitle("人生")
实际上是语法糖,实际执行的是 (&book).SetTitle(&book, "人生")
,所以传入指针接收者方法的对象地址和调用者地址一样。
同理:
// 改成值方法
func (b Book) SetTitle(nv string) {
b.title = nv
}
// ...
book2 := &Book{"西游记", "吴承恩"}
book2.SetTitle("人生") // 实际上是 (*book2).SetTitle(&book, "人生") `
如果调用者是指针对象,实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针,但是指针指向同一个对象。
编译器里面的 self
在很多其它面向对象的编程语言中,属主参数名总是为隐式声明的 this
或者 self
。这样的名称不推荐在 Go 编程中使用。那说回来,那 Go 是如何修改接收者内的值的呢?
如下例子:
type Book struct {
pages int
}
func (b Book) Pages() int {
return b.pages
}
func (b *Book) SetPages(pages int) {
b.pages = pages
}
编译器将自动声明下面的两个函数:
func Book.Pages(b Book) int {
return b.pages // 此函数体和Book类型的Pages方法体一样
}
func (*Book).SetPages(b *Book, pages int) {
b.pages = pages // 此函数体和*Book类型的SetPages方法体一样
}
可以看到,Go 实际上是把这个对象通过入参的方式传递进来
指针方法和值方法
1、如果实现了接收者是值类型的方法,会隐含地也实现接收者是指针类型的方法
2、值方法(value methods)可以通过指针和值调用,但是指针方法(pointer methods)只能通过指针来调用。
但是注意:实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。
如下例子:
type coder interface {
code()
debug()
}
type Gopher struct {
language string
}
func (p Gopher) code() {
fmt.Printf("I am coding %s language\n", p.language)
}
func (p *Gopher) debug() {
fmt.Printf("I am debuging %s language\n", p.language)
}
func main() {
var c coder = &Gopher{"Go"}
c.code()
c.debug()
}
运行一下,结果:
I am coding Go language
I am debuging Go language
但是如果我们把 main 函数的第一条语句换一下(改成值):
func main() {
var c coder = Gopher{"Go"}
c.code()
c.debug()
}
运行一下,报错:
./main.go:24:6: cannot use Programmer literal (type Programmer) as type coder in assignment:
Programmer does not implement coder (debug method has pointer receiver)
如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。反之无效
Go 中的 “继承” 与 “重写”
下面再介绍下 “继承” 的写法(Go 的这个其实是组合),go 支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段
type Human struct {
name string
sex string
}
func (receiver *Human) Eat() {
fmt.Println("Human Eat()...")
}
func (receiver *Human) Walk() {
fmt.Println("Human Walk()...")
}
/*
创建一个 SupperMan 继承 Human(其实是组合)
*/
type SupperMan struct {
Human // 这个也叫做 “匿名字段”
level int
}
// Eat 重写
func (receiver *SupperMan) Eat() {
fmt.Println("SupperMan Eat()...")
}
func (receiver SupperMan) Fly() {
fmt.Println("SupperMan Fly()...")
}
func main() {
h := Human{name: "zhang", sex: "man"}
h.Eat()
h.Walk()
// 定义子类
s := SupperMan{Human{name: "li", sex: "woman"}, 100}
s.Fly()
s.Eat()
}
打印结果
Human Eat()...
Human Walk()...
SupperMan Fly()...
SupperMan Eat()...
Go 中的 new 关键字
golang 内置函数 new()
和 struct{}
初始化的区别
new()
这是一个用来分配内存的内置函数,它的第一个参数是一个类型,不是一个值,它的返回值是一个指向新分配的 t 类型的零值的指针。
在 golang 的代码定义如下:
func new(t Type) *Type
直接使用 struct{}
来初始化 strut 时,返回的是一个 struct 类型的值,而不是指针两者是不一样的,两者对比代码如下:
type Student struct {
id int
name string
}
func main(){
var s_1 *Student = new(Student)
s_1.id = 100
s_1.name = "cat"
var s_2 Student = Student{id:1, name:"tom"}
fmt.Println(s_1, s_2)
// 输出结果:&{100 cat} {1 tom}
}
Go 中的接口
Go 中的接口和 TypeScript 的一样,并没有向 Java 那样严格要求必须继承,只需要实现这个接口的方法就行了
- 在 Java 中:实现接口需要显式地声明接口并实现所有方法;
- 在 Go 中:实现接口的所有方法就隐式地实现了接口;
定义接口需要使用 interface
关键字,在接口中我们只能定义方法签名,不能包含成员变量
// 例如异常的那个接口
type error interface {
Error() string
}
如果一个类型需要实现 error 接口,那么它只需要实现 Error() string
方法,下面的 RPCError 结构体就是 error 接口的一个实现:
type RPCError struct {
Code int64
Message string
}
func (e *RPCError) Error() string {
return fmt.Sprintf("%s, code=%d", e.Message, e.Code)
}
Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查
func main() {
var rpcErr error = NewRPCError(400, "unknown err") // typecheck1
err := AsErr(rpcErr) // typecheck2
println(err)
}
func NewRPCError(code int64, msg string) error {
return &RPCError{ // typecheck3
Code: code,
Message: msg,
}
}
func AsErr(err error) error {
return err
}
显式实现接口的小技巧
Golang 实现接口无需像 Java 那样显示的标注自己是否实现了某个接口,所以有时候可能会出现忘记实现某个接口的情况。
这种时候可以使用下面这种写法
var _ PeerGetter = (*httpGetter)(nil)
这是确保接口被实现常用的方式。即利用强制类型转换,确保 struct httpGetter
实现了接口 PeerGetter
。这样 IDE 和编译期间就可以检查,而不是等到使用的时候。
“万能” 接口和断言
Go 中的 interface{}
代表任意类型
type Book struct {
auth string
}
func call(arg interface{}) {
fmt.Printf("%v \n", arg)
}
func main() {
call(Book{auth: "张三"})
call(1000)
call("李四")
}
那 interface{}
如何区分传入的类型呢?这时就要使用 Go 的断言了
func call(arg interface{}) {
val, ok := arg.(string)
if !ok {
fmt.Printf("arg is not string type %T \n", arg) // %T 打印某个类型的完整说明
} else {
fmt.Printf("arg is string type %s \n", val)
}
}
func main() {
call(Book{auth: "张三"})
call(1000)
call("李四")
}
打印:
arg is not string type main.Book
arg is not string type int
arg is string type 李四
这个打印的关键字参考 Go 语言中的格式化输出
Go 中的 “构造函数”
构造函数是一种特殊的方法,主要用来在创建对象时初始化对象,即为对象成员变量赋初始值。特别的一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同来区分它们,即构造函数的重载。
Golang 结构体里面没有构造函数,所以可以像 Java 的 Setter、Getter 那样遵循一个命名规范
示例:
定义一个结构
type ContentMsg struct {
EffectId int `json:"effect_id"`
Text string `json:"text"`
Data interface{} `json:"data"`
}
通过 new 一个对象,或者利用 Golang 本身的 &
方式来生成一个对象并返回一个对象指针:
func NewContentMsg(data, effectId int) *ContentMsg {
instance := new(ContentMsg) // new 传入的是一个类型
instance.Data = data
instance.EffectId = effectId
return instance
}
不过这个本质就是一个静态工厂模式
Reference
值接收者和指针接收者的区别 Receiver Type 34. 图解 Go 语言:静态类型与动态类型 Go 语言设计与实现 深度解密Go语言之关于 interface 的 10 个问题